# This file is part of PyMXit.
# 
# PyMXit is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# PyMXit is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with PyMXit.  If not, see <http://www.gnu.org/licenses/>.


import socket
import datetime
import logging
import time
import gobject
from exceptions import Exception
from Queue import Queue, Empty


from mxit.enum import Enumeration
from mxit.handles import *
from mxit.encryption import client_id, encode_password
from mxit.events import EventManager
from mxit.errors import MxitException
from mxit.commands import Client, Server, Binary
from mxit.messages import *

logger = logging.getLogger("libmxit.connection")


class MxitConnection:
    
    def __init__(self, **params):
        """
        Create a new connection.
        
        Parameters and defaults:
            host -> 'stream.mxit.co.za'
            port -> 9119
            id -> ''
            password -> ''
            client_id -> '' (from the jad file)
            features -> ''
            java_env -> 'E-5.3.0'
            revision -> 'v5_6'
        """
        
        defaults = {
            'host': 'stream.mxit.co.za',
            'port': 9119,
            'id': '',
            'password': '',
            'client_id': '',
            'features': '',
            'revision': 'v5_6',
            'java_env': 'E-5.3.0',
            'language': 'en',
            'country_code': 27,
            
            'response_timeout': 20.0,        # s
            'ping_interval': 10*60*1000,    # ms
        }
        defaults.update(params)
        for key, value in defaults.items():
            setattr(self, key, value)
        self.roster = Roster()
        self.queue = Queue()
        self.waiting_for = None
        self.listeners = EventManager()
        self.status = Status.DISCONNECTED
        
        
    
    # ----------------
    # PUBLIC FUNCTIONS
    # ----------------
    
    def connect(self):
        """ Connect, login, and listen. """
        
        if self.status != Status.DISCONNECTED:
            raise MxitException('Already connected')
        self._set_status(Status.CONNECTING, reason=StatusChangeReason.REQUESTED)
        
        self.buffer = ""
        try:
            self._connect()
        except Exception, e:
            logger.exception(e)
            self._set_status(Status.DISCONNECTED, reason=StatusChangeReason.NETWORK_ERROR)
            
            return
        
        self._set_status(Status.AUTHENTICATING, reason=StatusChangeReason.REQUESTED)
        
        try:
            self._do_login()
        except Exception, e:
            logger.exception(e)
            self._set_status(Status.DISCONNECTED, reason=StatusChangeReason.ERROR)
            return
        
        
        gobject.io_add_watch(self.sock, gobject.IO_IN, self.listen_once)
        gobject.timeout_add(self.ping_interval, self._ping)
        
    def close(self):
        """ Logout and close the connection. """
        self.s(LogoutMessage)
        self._set_status(Status.DISCONNECTING, StatusChangeReason.REQUESTED)
        
        #TODO: this can be implemented in the message timeout
        def timeout_cb():
            if self.status != Status.DISCONNECTED:
                logger.error("Logout timed out")
                self._close()
                self._set_status(Status.DISCONNECTED, StatusChangeReason.REQUESTED)
        gobject.timeout_add(5000, timeout_cb)
        
        
    def message(self, buddy, message, type=MessageType.TEXT):
        """ Send a message to the specified buddy. """
        self.s(TextMessage, Message(buddy, message, type=type))
        
    
    def set_presence(self, presence):
        """ Set the user's presence.
        
        If the client is not connected yet, the presence is sent after login.
        """
        self.roster.self_buddy.presence = presence
        
        if self.status == Status.ACTIVE:
            self.s(PresenceMessage, presence)
        # If not running, the presence will be set when connected
        
    def edit_buddy(self, buddy):
        """ Update the group and name of a buddy on the server.
        
        Use update_buddy instead when possible.
        """
        if buddy.is_listed():
            self.s(EditMessage, buddy, name=buddy.name, group=buddy.group)
        
    
    def invite(self, jid, name=None, group=None):
        """ Invite a buddy.
        
        The buddy is only added to the roster when we received a response from the server.
        
        The jid that the server sends back might be different from the one we invited.
        """
        
        if not name:
            name = jid
        if not group:
            group = self.roster.root_group()

        logger.info("Inviting: %s" % jid)
        self.s(BuddyInviteMessage, jid, name, group)
    
    def accept_invite(self, buddy):
        """ Accept an invitation from a buddy.
        
        The buddy is also placed in its group, and named.
        """
        if buddy.presence != Presence.PENDING:
            logger.error("Attempting to accept invitation from %s, but no invitation was received" % buddy.jid)
        else:
            logger.info("Accepting invitation for %s" % buddy.jid)
            
            self.s(AcceptInviteMessage, buddy)
        
    def reject_buddy(self, buddy):
        if buddy.presence == Presence.PENDING:
            logger.error("Rejection not implemented yet")
        else:
            logger.error("Cannot reject %s" % buddy.jid)
    
    def remove_buddy(self, buddy):
        """ Remove a buddy. """
        if buddy.presence == Presence.PENDING:
            pass    # Reject buddy instead?
        
        if buddy.is_listed() and buddy.jid in self.roster.buddies:
            self.s(RemoveBuddyMessage, buddy)
    
        
    def update_buddy(self, buddy, name=None, group=None):
        """ Update a buddy on the server.
        
        The update is only sent to the server if the buddy's details changed.
        """
        
        if buddy.is_listed() and buddy.jid in self.roster.buddies:
            attrs = {}
            if name is not None:
                name = encode_string(name)
                if name != buddy.name:
                    attrs['name'] = name
                    
            if group is not None and buddy.group != group:
                attrs['group'] = group
                
            if attrs:
                self.s(EditMessage, buddy, **attrs)
        else:
            # Is this the correct way to handle it?
            buddy.name = name
            buddy.group = group
        
    
    def set_mood(self, mood):
        """ Set the user's mood. """
        self.roster.self_buddy.mood = mood
        self.s(MoodMessage, mood)
        
        
    # -----------------
    # UTILITY FUNCTIONS
    # -----------------
    
    def _enqueue(self, data):
        self.queue.put(data)
        gobject.idle_add(self.write_once)
        
    def _basic_command(self, command, data):
        self._enqueue(StandardMessage(self, command, data))
        
    def s(self, msgclass, *vargs, **kwargs):
        self._enqueue(msgclass(self, *vargs, **kwargs))
        
                      
    def _set_status(self, status, reason=StatusChangeReason.UNKNOWN):
        self.status = status
        logger.info("Status changed to %s for reason %s" % (status, reason))
        self.listeners.status_changed(status, reason)   
        
    def _do_login(self):
        """ Log in. """
        self.s(LoginMessage)
        presence = self.roster.self_buddy.presence
        if presence != Presence.AVAILABLE:
            logger.info("Initial presence: %s" % self.roster.self_buddy.presence)
            self._presence(presence.id)
           
        
    
    def _ping(self):
        self.s(PingMessage)
        gobject.timeout_add(self.ping_interval, self._ping)
        
    
        
    # -------------------
    # PROCESSING COMMANDS
    # -------------------
    
    def _r_invite(self, message):
        tree = message.data
        jid = tree[0][0]
        name = tree[0][1]
        
        type_id = tree[0][2]
        presence_id = tree[0][3]
        message = tree[0][4]
        random_name = tree[0][5]
        
        type = BuddyType.byid(type_id)
        
        if jid in self.roster.buddies:
            # We invited him?
            buddy = self.roster.buddies[jid]
            if buddy.is_subscribed():
                logger.info("We invited buddy or know buddy, auto accepting")
                self.s(AcceptInviteMessage, buddy)
                return
            else:
                logger.info("Buddy we know of invited us, not auto accepting")
                buddy.presence = Presence.PENDING
                self.listeners.buddy_updated(buddy, presence=buddy.presence)
        else:
            logger.info("Buddy invited, not auto accepting")
            # Buddy invited us
            buddy = Buddy(jid, name, self.roster.root_group(), type=type, presence=Presence.PENDING)
            self.roster.buddies[jid] = buddy
            self.listeners.buddy_added(buddy)
        if message:
            msg = Message(buddy, message, datetime.now(), MessageType.TEXT)
            self.listeners.message_received(msg)
            
    
    def _r_logout(self, message):
        self._close()
        logger.info("Logout received")
        if self.status == Status.DISCONNECTING:
            self._set_status(Status.DISCONNECTED, StatusChangeReason.REQUESTED)
        else:
            self._set_status(Status.DISCONNECTED)
        
    def _r_message(self, message):
        tree = message.data
        from_jid = tree[0][0]
        time = tree[0][1]
        time = datetime.fromtimestamp(int(time))
        type = MessageType.byid(int(tree[0][2]))
        msg = tree[1][0]
        buddy = self.roster.get_buddy(from_jid)
        if buddy.type == BuddyType.ROOM:
            s = msg.find('<')
            e = msg.find('>\n')
            if s == 0 and e > 0:
                sender_name = msg[s+1:e]
                sender = self.roster.find_buddy(sender_name)
                message = RoomMessage(buddy, msg, time=time, type=type, sender=sender)
            else:
                message = RoomMessage(buddy, msg, time=time, type=type)
            self.listeners.message_received(message)
        else:
            message = Message(buddy, msg, time=time, type=type)
            self.listeners.message_received(message)
        
        
    def _r_presence(self, message):
        tree = message.data
        for child in tree:
            if len(child) != 6:
                if len(child) > 1:
                    raise MxitException("Invalid length for presence command: %s", str(child))
                continue
            group, jid, name, presence, type, mood = child
            presence = Presence.byid(presence)
            group = self.roster.get_group(group)
            mood = Mood.byid(mood)
            type = BuddyType.byid(type)
            buddy, new, attrs = self.roster.update_buddy(jid, name=name, group=group, mood=mood, presence=presence, type=type)
            
            if new:
                self.listeners.buddy_added(buddy)
            else:
                if attrs:
                    self.listeners.buddy_updated(buddy, **attrs)
                
    
    # ----------------
    # SOCKET HANDLING
    # ----------------
    
    def listen_once(self, *vargs):
        """ Listen for data from the server.
        
        Must be called repetitively.
        """
        try:
            data = self.sock.recv(1024)
            if not data:
                self._close()
                if self.status != Status.DISCONNECTED:
                    self._set_status(Status.DISCONNECTED, StatusChangeReason.NETWORK_ERROR)
                
                return False
        except Exception, e:
            self._close()
            if self.status != Status.DISCONNECTED:
                self._set_status(Status.DISCONNECTED, StatusChangeReason.NETWORK_ERROR)
            return False
        
        self.buffer += data
        
        while len(self.buffer) > 0:
            if self.buffer.startswith('ln='):
                tmp = self.buffer.index('\0')
                l = int(self.buffer[3:tmp])
                logger.info("message of length %d" % l)
                if l+tmp > len(self.buffer):
                    break    # wait for data
                self.buffer = self.buffer[tmp+1:]
                
                temp = self.buffer[:l]
                while len(temp) > 0:
                    tmp = temp.index('\0')
                    cid = int(temp[:tmp])
                    if cid == Server.BINARY.id:
                        logger.info("Binary command, ignoring")
                        break
                    end = temp.find('\2')
                    if end == -1:
                        end = len(temp)
                    if end > 0:
                        self._parse_command(temp[:end])
                    temp = temp[end+1:]
                
                self.buffer = self.buffer[l:]
            else:
                #TODO
                logger.error("invalid start of message")
                i = self.buffer.find('ln=')
                if i >= 0:
                    self.buffer = self.buffer[i:]
                else:
                    self.buffer = ""
                    return True
        return True
           
        
    def _close(self):
        self.sock.close()
        
    
        
        
        
    def _parse_command(self, command):
        tree = create_tree(command)
        cmd_id = int(tree[0][0])
        response = build_response(tree[1])
        tree = tree[2:]
        cmd = Client.byid(cmd_id)    # First try client command - response
        
        message = ServerMessage(cmd, response, tree)
        
        logger.info("~ received ~ %d:%s/%s ~ %s ~ %s" % (cmd_id, cmd, Server.byid(cmd_id), response, tree))
        
        if self.waiting_for is not None:
            try:
                handled = self.waiting_for.received(message)
            except Exception, e:
                logger.exception(e)
                self.waiting_for = None
                return
            
            if handled:
                self.waiting_for = None
                return
            
        cmd = Server.byid(cmd_id)    # Now try server command
        message.cmd = cmd
        try:
            if cmd == Server.MESSAGE:
                self._r_message(message)
            elif cmd == Server.LOGOUT:
                self._r_logout(message)
            elif cmd == Server.PRESENCE:
                self._r_presence(message)
            elif cmd == Server.INVITE_RECEIVED:
                self._r_invite(message)
            else:
                logger.info("unknown command")
        except Exception, e:
            logger.exception(e)
            self.listeners.error("Error processing command", e)
    
        
    def write_once(self):
        # Loop until the queue is empty, or until we are waiting for a command
        while True:
            if self.queue.empty():
                return False
            if self.waiting_for is not None:
                logger.info("Still waiting for %s, not sending" % self.waiting_for)
                sent_at = self.waiting_for.sent_at
                if time.time() - sent_at > self.response_timeout:
                    logger.error("Timed out: %r" % self.waiting_for)
                    self.waiting_for.timeout()
                    self.waiting_for = None
                else:
                    gobject.timeout_add(500, self.write_once)
                    return False
        
            
            if self.status not in [Status.ACTIVE, Status.AUTHENTICATING, Status.DISCONNECTING]:
                logger.error("Not in a state to send data: %r" % (self.status))
                return False
            
            try:
                message = self.queue.get(block=False)
            except Empty:
                return False
            
            try:
                message.send(self.sock)
                if message.should_wait():
                    self.waiting_for = message
                    message.sent_at = time.time()
            except Exception, e:
                logger.exception(e)
                self._close()
                if self.status != Status.DISCONNECTED:
                    self._set_status(Status.DISCONNECTED, StatusChangeReason.NETWORK_ERROR)
            
                
            self.queue.task_done()

        
    def _connect(self):
        """ Connect to the server. """
        
        logger.info("Connecting to: %s:%d" % (self.host, self.port))
        self.sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        self.sock.connect((self.host,self.port))
    

    
class Response:
    def __init__(self, code, message=None):
        self.code = code
        self.message = message
        
    def __str__(self):
        return "%d: %s" % (self.code, self.message)
    
    def __repr__(self):
        return "<Reponse %s>" % str(self)
        
def build_response(branch):
    code = int(branch[0])
    msg = None
    if len(branch) > 1:
        msg = branch[1]
    return Response(code, msg)

def readable(data):
    data = data.replace('\0', '!')
    data = data.replace('\1', '#')
    data = data.replace('\2', '$')
    return data

def create_tree(data):
    """ Parse a command. """    
    return map(lambda s: s.split('\1'), data.split('\0'))
